"""
Async gRPC client for communicating with the Roslyn analyzer service.

This module provides a high-level async interface to the analyzer service.
"""

import asyncio
import json
import logging
from pathlib import Path
from typing import Optional

import grpc
from orchestrator.analyzer_client.generated import (
    analyzer_service_pb2,
    analyzer_service_pb2_grpc,
)
from orchestrator.analyzer_client.models import (
    DocumentSyntaxInfo,
    ProjectAnalysisResult,
    SolutionAnalysisResult,
)

logger = logging.getLogger(__name__)


class AnalyzerClientError(Exception):
    """Base exception for analyzer client errors"""

    pass


class ConnectionError(AnalyzerClientError):
    """Raised when connection to analyzer service fails"""

    pass


class AnalysisError(AnalyzerClientError):
    """Raised when analysis fails"""

    pass


class AnalyzerClient:
    """
    Async gRPC client for the Roslyn analyzer service.

    This client provides methods to analyze C# solutions and retrieve
    syntax information in JSON format.

    Example:
        async with AnalyzerClient("localhost:50051") as client:
            result = await client.analyze_solution("/path/to/solution.sln")
            for project in result.projects:
                print(f"Analyzed project: {project.name}")
    """

    def __init__(
        self,
        server_address: str = "localhost:50051",
        timeout: float = 30.0,
        max_message_length: int = 100 * 1024 * 1024,  # 100MB
    ) -> None:
        """
        Initialize the analyzer client.

        Args:
            server_address: gRPC server address (host:port)
            timeout: Default timeout for requests in seconds
            max_message_length: Maximum message size for gRPC
        """
        self.server_address = server_address
        self.timeout = timeout
        self._channel: Optional[grpc.aio.Channel] = None
        self._stub: Optional[analyzer_service_pb2_grpc.AnalyzerServiceStub] = None
        self._max_message_length = max_message_length

    async def __aenter__(self) -> "AnalyzerClient":
        """Async context manager entry"""
        await self.connect()
        return self

    async def __aexit__(self, exc_type: type, exc_val: Exception, exc_tb: object) -> None:
        """Async context manager exit"""
        await self.close()

    async def connect(self) -> None:
        """
        Establish connection to the analyzer service.

        Raises:
            ConnectionError: If connection fails
        """
        try:
            # Configure channel options for large messages
            options = [
                ("grpc.max_receive_message_length", self._max_message_length),
                ("grpc.max_send_message_length", self._max_message_length),
            ]

            self._channel = grpc.aio.insecure_channel(self.server_address, options=options)
            self._stub = analyzer_service_pb2_grpc.AnalyzerServiceStub(self._channel)

            # Test connection with a short timeout
            await asyncio.wait_for(self._channel.channel_ready(), timeout=5.0)
            logger.info(f"Connected to analyzer service at {self.server_address}")

        except asyncio.TimeoutError as e:
            raise ConnectionError(
                f"Connection timeout: Could not connect to {self.server_address}"
            ) from e
        except Exception as e:
            raise ConnectionError(f"Failed to connect to {self.server_address}: {e}") from e

    async def close(self) -> None:
        """Close the connection to the analyzer service"""
        if self._channel:
            await self._channel.close()
            self._channel = None
            self._stub = None
            logger.info("Disconnected from analyzer service")

    async def analyze_solution(
        self,
        solution_path: str | Path,
        project_names: Optional[list[str]] = None,
        timeout: Optional[float] = None,
    ) -> SolutionAnalysisResult:
        """
        Analyze a C# solution.

        Args:
            solution_path: Path to the .sln file
            project_names: Optional list of specific projects to analyze
            timeout: Request timeout (uses default if not specified)

        Returns:
            SolutionAnalysisResult containing analyzed syntax information

        Raises:
            ConnectionError: If not connected to service
            AnalysisError: If analysis fails
        """
        if not self._stub:
            raise ConnectionError("Not connected to analyzer service. Call connect() first.")

        solution_path = Path(solution_path).resolve()
        if not solution_path.exists():
            raise AnalysisError(f"Solution file not found: {solution_path}")

        # Create request
        request = analyzer_service_pb2.SolutionRequest(
            solution_path=str(solution_path),
            project_names=project_names or [],
        )

        # Call service
        try:
            request_timeout = timeout or self.timeout
            response = await asyncio.wait_for(
                self._stub.AnalyzeSolution(request), timeout=request_timeout
            )
            logger.info(f"Successfully analyzed solution: {response.solution_name}")

        except asyncio.TimeoutError as e:
            raise AnalysisError(
                f"Analysis timeout after {request_timeout}s for {solution_path}"
            ) from e
        except grpc.aio.AioRpcError as e:
            raise AnalysisError(f"gRPC error during analysis: {e.details()}") from e
        except Exception as e:
            raise AnalysisError(f"Unexpected error during analysis: {e}") from e

        # Parse response
        return self._parse_response(response)

    def _parse_response(
        self, response: analyzer_service_pb2.SolutionResponse
    ) -> SolutionAnalysisResult:
        """
        Parse gRPC response into Pydantic models.

        Args:
            response: gRPC response

        Returns:
            SolutionAnalysisResult
        """
        projects: list[ProjectAnalysisResult] = []

        for project_info in response.projects:
            documents: list[DocumentSyntaxInfo] = []

            for doc_info in project_info.documents:
                # Parse JSON syntax information
                try:
                    syntax_data = json.loads(doc_info.syntax_json)
                    doc_syntax = DocumentSyntaxInfo(**syntax_data)
                    documents.append(doc_syntax)
                except json.JSONDecodeError as e:
                    logger.error(f"Failed to parse JSON for {doc_info.name}: {e}")
                except Exception as e:
                    logger.error(f"Failed to create DocumentSyntaxInfo for {doc_info.name}: {e}")

            projects.append(
                ProjectAnalysisResult(
                    name=project_info.name, path=project_info.path, documents=documents
                )
            )

        return SolutionAnalysisResult(
            solution_name=response.solution_name,
            projects=projects,
            errors=list(response.errors),
        )

    async def ping(self) -> bool:
        """
        Check if the analyzer service is reachable.

        Returns:
            True if service is reachable, False otherwise
        """
        if not self._channel:
            return False

        try:
            await asyncio.wait_for(self._channel.channel_ready(), timeout=2.0)
            return True
        except (asyncio.TimeoutError, Exception):
            return False
